<?php
//-------------------------------------------------------------------------
// OVIDENTIA http://www.ovidentia.org
// Ovidentia is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or (at your option)
// any later version.
// 
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
// USA.
//-------------------------------------------------------------------------
/**
 * @license http://opensource.org/licenses/gpl-license.php GNU General Public License (GPL)
 * @copyright Copyright (c) 2006 by CANTICO ({@link http://www.cantico.fr})
 */
include_once 'base.php';


require_once dirname(__FILE__) . '/form.class.php';


/**
 * Constructs a Widget_CsvImportForm.
 *
 * @param Widget_Layout	$layout		The layout that will manage how widgets are displayed in this container.
 * @param string		$id			The item unique id.
 * @return Widget_Form
 */
function Widget_CsvImportForm($id = null, Widget_Layout $layout = null)
{
	return new Widget_CsvImportForm($id, $layout);
}


class Widget_CsvImportForm extends Widget_Form 
{
	private $import_fields = array();
	private $mandatory_fields = array();
	
	private $error_message = null;
	
	/**
	 * Add a field to import to
	 * 
	 * @param	string	$name
	 * @param	string	$label
	 * @param	bool	$mandatory
	 * 
	 * @return	Widget_CsvImportForm
	 */ 
	public function addImportField($name, $label, $mandatory = false)
	{
		assert('is_string($label); /* The "label" parameter must be a string */');
		$this->import_fields[$name] = $label;
		
		if ($mandatory) {
			$this->mandatory_fields[$name] = $label;
		}
		
		
		return $this;
	}
	
	/**
	 * set an array as fields description
	 * @param	arrray	$arr key is name value is label
	 * 
	 * @return 	Widget_CsvImportForm
	 */ 
	public function setImportFields($arr) 
	{
		foreach($arr as $name => $label) 
		{
			$this->addImportField($name, $label);
		}
		
		return $this;
	}
	
	
	/**
	 * Get a temporary path to write or read the csv data
	 * 
	 * @throw bab_FolderAccessRightsException | Exception
	 * 
	 * @return	bab_Path
	 */ 
	private function getTempPath()
	{
		require_once $GLOBALS['babInstallPath'].'utilit/path.class.php';
		
		$addon = bab_getAddonInfosInstance('widgets');
		$path = new bab_Path($addon->getUploadPath());
		$path->push(__CLASS__);
		$path->createDir();
		
		// throw exception if the folder is not created of not writable
		$path->isFolderWriteable();
		
		return $path;
	}
	
	/**
	 * Remove expired temporary files and the current session temporary file
	 * @return	Widget_CsvImportForm
	 */ 
	private function cleanup()
	{
		$sessions = array();
		foreach(bab_getActiveSessions() as $arr) {
			$sessions[$arr['session_id']] = 1;
		}

		if (isset($sessions[session_id()])) {
			unset($sessions[session_id()]);
		}
		
		$path = $this->getTempPath()->tostring();
		$d = dir($path);
		while (false !== ($entry = $d->read())) {
			if (!isset($sessions[$entry]) && !is_dir($path.'/'.$entry)) {
				unlink($path.'/'.$entry);
			}
		}
		
		return $this;
	}
	
	
	
	/**
	 * get the temporary CSV file with data to import, if no file loaded return null
	 * @return string | null		full path to file
	 */ 
	public function getTemporaryFile()
	{
		$path = $this->getTempPath()->tostring();
		$file = $path.'/'.session_id();
		
		if (!file_exists($file)) {
			return null;
		}

		return $file;
	}
	
	
	/**
	 * test if all import parameters are available to do the mapping step
	 * @return bool
	 */ 
	public function importMappingParams()
	{
		return (
			null !== bab_rp('import_delimiter', null) 
			&& null !== bab_rp('import_enclosure', null) 
			&& null !== bab_rp('import_encoding', null) 
			&& null !== $this->getTemporaryFile()
		);
	}
	
	/**
	 * Add all parameters required to import a csv file on an action
	 */ 
	public function setActionParameters(Widget_Action $action)
	{
		$params = array(
			'import_delimiter',
			'import_enclosure',
			'import_encoding',
			'import_mapping',
			'import_first_line'
		);
		
		foreach($params as $varname) 
		{
			$action->setParameter($varname, bab_rp($varname));
		}
	}
	
	
	/**
	 * 
	 * @return Widget_Frame
	 */ 
	protected function labelField(Widget_InputWidget $widget, $label)
	{
		$W = bab_functionality::get('Widgets');
		$widgetLabel = $W->Label($label.' :')->setAssociatedWidget($widget);
		
		$frame = $W->Frame(null, $W->VBoxLayout());
		
		$frame->addItem($widgetLabel);
		$frame->addItem($widget);
		
		return $frame;
	}
	
	
	/**
	 * Prepare the fields for file loader
	 * 
	 * add :
	 *  input type file
	 *  select for separator choice
	 *  select for encoding choice
	 *  a submit button
	 * 
	 * @return string
	 */ 
	protected function prepareLoader()
	{
		$this->cleanup();
		
		$W = bab_functionality::get('Widgets');
		
		
		$uploader = $W->Uploader()->setName('import_file');
		$delimiter = $W->Select()->setName('import_delimiter')->setOptions(
			array(
				';' 	=> widget_translate("Semicolon"),
				',' 	=> widget_translate("Comma"),
				"{tab}" => widget_translate("Tab")
			)
		);
		
		$enclosure = $W->Select()->setName('import_enclosure')->setOptions(
			array(
				'"' 	=> widget_translate('Double quotation mark'),
				'`' 	=> widget_translate('Accent (`)')
			)
		);
		
		$encoding = $W->Select()->setName('import_encoding')->setOptions(
			array(
				'WINDOWS-1252'				=> 'WINDOWS-1252',
				bab_charset::ISO_8859_15	=> bab_charset::ISO_8859_15,
				bab_charset::UTF_8 			=> bab_charset::UTF_8
			)
		);
		
		
		
		
		$this->getLayout()->setVerticalSpacing(1, 'em');
		
		if ($this->error_message) {
			$this->addItem($W->Title($this->error_message)->addClass('error'));
		}
		
		
		$this->addItem($this->labelField($uploader, widget_translate("Import CSV file")));
		$this->addItem(
			$W->HBoxItems(
				$this->labelField($delimiter, widget_translate("Field delimiter")),
				$this->labelField($enclosure, widget_translate("Field enclosure character"))
				
			)->setHorizontalSpacing(2, 'em')
		);
		$this->addItem($this->labelField($encoding, widget_translate("File encoding")));
		$this->addItem($W->SubmitButton()->setLabel(widget_translate('Next')));
		
		$this->setValues($_POST);
	}
	
	/**
	 * Prepare the form fields for mapping
	 * 
	 * add :
	 *  one select for each import fields
	 *  a submit button
	 * 
	 * @return string
	 */ 
	protected function prepareMapping()
	{
		$W = bab_functionality::get('Widgets');
		
		$mapping = $W->TableView()->setName('import_mapping')->setView(Widget_TableView::VIEW_LIST);
		$options = $this->getCsvColumns();
		$options[''] = '';
		
		/* Sort options in select inputs */
		bab_Sort::natcasesort($options);
		
		$position = 0;
		/* List at first mandatory fields */
		/* $mapping : Table ; Position 0 : left column ; Position 1 : right column */
		$mapping->addItem($W->Title(widget_translate('Mandatory fields :'), 4), $position, 0);
		$mapping->addItem($W->Html(), $position, 1);
		$position++;
		foreach($this->import_fields as $name => $label) {
			if (isset($this->mandatory_fields[$name])) {
				$widgetLabel = $W->Label($label.' :');
				$select = $W->Select()->setName($name)->setOptions($options)->setAssociatedLabel($widgetLabel);
				
				$widgetLabel->addClass('widget-csv-import-form-mandatory');
				$select->addClass('widget-csv-import-form-mandatory')->setMandatory(true, widget_translate('This field is mandatory'));
				
				/* $mapping : Table ; Position 0 : left column ; Position 1 : right column */
				$mapping->addItem($widgetLabel, $position, 0);
				$mapping->addItem($select, $position, 1);
				$position++;
			}
		}
		/* List optionnal fields */
		/* $mapping : Table ; Position 0 : left column ; Position 1 : right column */
		$mapping->addItem($W->Title(widget_translate('Optional fields :'), 4), $position, 0);
		$mapping->addItem($W->Html(), $position, 1);
		$position++;
		foreach($this->import_fields as $name => $label) {
			if (!isset($this->mandatory_fields[$name])) {
				$widgetLabel = $W->Label($label.' :');
				$select = $W->Select()->setName($name)->setOptions($options)->setAssociatedLabel($widgetLabel);
				
				/* $mapping : Table ; Position 0 : left column ; Position 1 : right column */
				$mapping->addItem($widgetLabel, $position, 0);
				$mapping->addItem($select, $position, 1);
				$position++;
			}
		}
		
		$this->getLayout()->setVerticalSpacing(1, 'em');
		
		if ($this->error_message) {
			$this->addItem($W->Title($this->error_message)->addClass('error'));
		}
		
		$this->addItem($mapping);
		
		$first_line = $W->Checkbox()->setName('import_first_line');
		$first_line_label = $W->Label(widget_translate('Import also the first line of the CSV file'))->setAssociatedWidget($first_line);
		
		$this->addItem($W->HBoxItems($first_line, $first_line_label)->setVerticalAlign('middle'));
		$this->addItem(

			$W->SubmitButton()->setName('next')->setLabel(widget_translate('Next'))->validate(true)
		);
		
		if (bab_pp('import_mapping')) {
			$this->setValues($_POST);
		} else {
			// try to evaluate a default mapping
			
			$default_mapping = array();
			$fliped_options = array_flip($options);
			
			
			foreach($this->import_fields as $name => $label) {
				if (isset($fliped_options[$label])) {
					$default_mapping[$name] = $fliped_options[$label];
				}
			}

			
			$default = array(
				'import_mapping' => $default_mapping
			);
			
			$this->setValues($default);
		}
	}
	
	/**
	 * Open CSV file
	 * @return array
	 */ 
	private function getCsvHandle()
	{
		$file = $this->getTemporaryFile();
		
		if (null === $file) 
		{
			throw new Exception('No CSV file loaded');
			return null;
		}
		
		if (false === $handle = fopen($file, 'r'))
		{
			throw new Exception(sprintf('Error while opening temporary CSV file %s', $file));
			return null;
		}
		
		return $handle;
	}
	
	
	private function getCsvColumns()
	{
		$handle = $this->getCsvHandle();
		
		if (null === $handle) {
			return array();
		}
		
		$delimiter = bab_rp('import_delimiter', null);
		$enclosure = bab_rp('import_enclosure', null);
		
		if (null === $delimiter || null === $enclosure) {
			throw new Exception('missing parameters');
			return array();
		}
		
		if ('{tab}' === $delimiter) {
			$delimiter = "\t";
		}
		
		$data = fgetcsv($handle, 0, $delimiter, $enclosure);
		
		return $data;
	}
	
	
	
	private function processUpload()
	{
		require_once $GLOBALS['babInstallPath'].'utilit/uploadincl.php';
		$upload = bab_fileHandler::upload('import_file');
		
		if (false === $upload) {
			// no file uploaded
			return;
		}
		
		if ($upload->error) {
			$this->error_message = $upload->error;
			return;
		}
		
		$path = $this->getTempPath()->tostring();
		$file = $path.'/'.session_id();
		
		
		$upload->import($file);
	}
	
	
	
	/**
	 * The item classes.
	 *
	 * @return array
	 */
	public function getClasses()
	{
		$classes = parent::getClasses();
		$classes[] = 'widget-csv-import-form';
		return $classes;
	}
	
	
	/**
	 * Return columns mapping or null if the form is not on mapping step
	 * @return array		keys are columns names, values are positions in csv file
	 */ 
	public function getMapping()
	{
		if (false === bab_rp('import_mapping', false)) {
			return null;
		}
		
		if (null === $this->getTemporaryFile()) {
			return null;
		}
		
		$map = bab_rp('import_mapping');
		
		return $map;
	}
	
	
	
	/**
	 * Check the mapping array and return true if the mandatory fields has been submited
	 * @param	Array	$mapping
	 * @return	bool
	 */ 
	private function checkMandatory(Array $mapping)
	{
		foreach($mapping as $name => $position) {
			if (isset($this->mandatory_fields[$name]) && '' === (string) $position) {
				
				$this->error_message = sprintf(
					widget_translate('The field %s must be associated to a CSV column'),
					$this->import_fields[$name]
				);
				
				return false;
			}
		}
		
		return true;
	}
	
	
	
	/**
	 * Get result iterator if CSV file has been loaded correctly
	 * @return Widget_CsvIterator | null
	 */ 
	public function getIterator()
	{
		if (null === $mapping = $this->getMapping()) {
			return null;
		}
		
		if (!$this->checkMandatory($mapping)) {
			return null;
		}
		
		$handle = $this->getCsvHandle();
		$filesize = filesize($this->getTemporaryFile());
		$rewind_pos = 1;
		
		if (bab_rp('import_first_line')) {
			$rewind_pos = 0;
		}
		
		$delimiter = bab_rp('import_delimiter');
		
		if ('{tab}' === $delimiter) {
			$delimiter = "\t";
		}
		
		$iterator = new Widget_CsvIterator(
			$handle, 
			$filesize,
			$rewind_pos, 
			$mapping,
			$this->mandatory_fields,
			$delimiter,
			bab_rp('import_enclosure'),
			bab_rp('import_encoding')
		);
		
		
		
		
		return $iterator;
		
	}
	
	/**
	 * Get a CSV iterator from a generic csv file 
	 * 
	 * @param	string		$filepath		absolute full path to a CSV file
	 * @param	int			$rewind_pos		initialial line number in CSV file (can be used to ignore the first line with $rewind_pos=1)
	 * @param 	array		$mapping		keys are columns names, values are columns positions in csv file
	 * @param	string		$delimiter		delimiter (optional, default value : , )
	 * @param	string		$enclosure		delimiter (optional, default value : " )
	 * @param	string		$encoding		ISO encoding code of CSV file content (optional, default value : UTF-8 )
	 * 
	 * @return Widget_CsvIterator
	 */
	public function getFileIterator($filepath, $rewind_pos, Array $mapping, $delimiter = ',', $enclosure = '"', $encoding = bab_charset::UTF_8)
	{
		
		if (false === $handle = fopen($filepath, 'r'))
		{
			throw new Exception(sprintf('Error while opening CSV file %s', $filepath));
			return null;
		}
		
		
		return new Widget_CsvIterator(
			$handle, 
			filesize($filepath),
			$rewind_pos, 
			$mapping,
			array(),
			$delimiter,
			$enclosure,
			$encoding
		);
	}
	
	
	
	
	public function display(Widget_Canvas $canvas)
	{
		if (bab_rp('previous')) {
			$this->cleanup();
			
		}
		
		$this->processUpload();
		
		
		if (false === $this->importMappingParams()) {
			$this->prepareLoader();
		} else {
			$this->prepareMapping();
		}
		
		$this->setSelfPageHiddenFields();
		
		
		return parent::display($canvas);
	}
}



/**
 * CSV row object
 */ 
class Widget_CsvRow { 
	
	private $__line_number = null;
	private $__mandatory_fields = null;
	
	/**
	 * @param	int		$line		line number in CSV file
	 * @param	array	$mandatory	array of mandatory fields to check with the checkMandatory method
	 */ 
	public function __construct($line, $mandatory)
	{
		$this->__line_number 		= $line;
		$this->__mandatory_fields 	= $mandatory;
	}
	
	/**
	 * Line number in CSV file
	 * @return	int
	 */ 
	public function line()
	{
		return $this->__line_number;
	}
	
	/**
	 * Check mandatory fields as specified in mapping
	 * return true if the row is valid
	 * 
	 * @throw Widget_ImportException
	 * 
	 * @return bool		
	 */ 
	public function checkMandatory()
	{
		foreach(get_object_vars($this) as $name => $value) {
			
			if (isset($this->__mandatory_fields[$name]) && '' === $value) {
				
				$exception = new Widget_ImportException(
					sprintf(
						widget_translate('The field %s is mandatory to import, the value is missing on line %d of the CSV file'), 
						$this->__mandatory_fields[$name], 
						$this->__line_number
					)
				);
				
				$exception->setCsvRow($this);
				
				throw $exception;
				
				return false;
			}
		}
		
		return true;
	}
}



/**
 * CSV result iterator
 * to browse the content of the uploaded CSV file
 */ 
class Widget_CsvIterator implements Iterator
{
	private $handle 				= null;
	private $filesize				= null;
	private $rewind_pos 			= 0;
	private $mapping 				= null;
	private $delimiter 				= null;
	private $enclosure 				= null;
	private $encoding 				= null;
	private $current_line 			= null;
	private $current_line_number	= null;
	
	
	/**
	 * Constructor
	 * 
	 * @param	ressource	$handle			link to opened CSV file
	 * @param	int			$filesize		CSV file size
	 * @param	int			$rewind_pos		line number in csv file to start iterator 
	 * @param	Array		$mapping		columns to export
	 * @param	string		$delimiter		CSV delimiter
	 * @param	string		$enclosure		CSV string enclosure
	 * @param	string		$encoding		CSV file content encoding ISO code
	 * 
	 */ 
    public function __construct($handle, $filesize, $rewind_pos, Array $mapping, Array $mandatory, $delimiter, $enclosure, $encoding)
    {
        $this->handle 		= $handle;
        $this->filesize		= $filesize;
        $this->rewind_pos	= $rewind_pos;
        $this->mapping 		= $mapping;
        $this->mandatory	= $mandatory;
        $this->delimiter 	= $delimiter;
        $this->enclosure 	= $enclosure;
        $this->encoding 	= $encoding;

    }

    public function rewind() {
		
        fseek($this->handle, 0);
        $this->next();
        
        $this->current_line_number = 1;
        
        for ($i = 0 ; $i < $this->rewind_pos; $i++) {
			$this->next();
			if (($i +1) < $this->rewind_pos) {
				$this->current();
			}
		}
    }


	/**
	 * 
	 * @return Widget_CsvRow
	 */ 
    public function current() {
        
        $row = new Widget_CsvRow($this->current_line_number, $this->mandatory);
        
        foreach($this->mapping as $name => $position) {

			if (!isset($this->current_line[$position])) {
				continue;
			}
			
			$value 	= $this->current_line[$position];
			$row->$name = $this->getStringAccordingToDataBase($value);
			
			/* !!! Do not use UNSET because it's possible that in the mapping, the same column of the file CSV can be several times used ! */
//			unset($this->current_line[$position]);
		}

        return $row;
    }
    
    /**
     * Convert charset
     * try transliteration if possible or use core function
     * 
     * @param string $value
     * @return string
     */
    private function getStringAccordingToDataBase($value)
    {
    	if (function_exists('iconv')) {
    		$input = $this->encoding;
    		$output = bab_charset::getIso();
    		return iconv($input, $output.'//TRANSLIT', $value);
    	}
    	
    	return bab_getStringAccordingToDataBase($value, $this->encoding);
    }
    
   

    public function key() {
        return ftell($this->handle);
    }

    public function next() {
		$this->current_line = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure);
		$this->current_line_number++;
    }

	/**
	 * 
	 * @return bool
	 */ 
    public function valid() {
		return (isset($this->current_line) && false !== $this->current_line);
    }
    
    /**
     * Get progress in percentage
     * @return int
     */ 
    public function getProgress() {
    	$nb = round(($this->key() * 100) / $this->filesize);
    	if ($nb > 100) {
    		$nb = 100;
    	}
		return $nb;
	}
}




/**
 * 
 * 
 */ 
class Widget_ImportException extends Exception
{
	private $csvRow = null;
	
	/**
	 * 
	 */ 
	public function setCsvRow(Widget_CsvRow $row)
	{
		$this->csvRow = $row;
	}
	
	/**
	 * 
	 * @return	Widget_CsvRow
	 */ 
	public function getCsvRow()
	{
		return $this->csvRow;
	}
	
	public function __destruct()
	{
		$this->csvRow = null;
	}
}
